test(benchmarks): add C#/ASP.NET and TypeScript tasks
Six tasks with hidden checks: ASP.NET validation and 404 handling, C# cancellation, and TypeScript overlap, deep merge, and debounce fixes, checked with dotnet run and node --test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
97879fd6a5
commit
f43a2df14b
30 files changed
+349
-2
No files matched your search
@@ -33,7 +33,8 @@ import time
|
||||
TASKS = Path(__file__).resolve().parent / 'tasks'
|
||||
CHECK = [sys.executable, '-m', 'unittest', 'discover', '-s', 'bench_hidden', '-t', '.']
|
||||
CLAUDE_TOOLS = ['Read', 'Edit', 'Write', 'Glob', 'Grep', 'Agent', 'Skill', 'TodoWrite', 'Bash(python *)', 'Bash(git *)',
|
||||
'Bash(ls *)', 'PowerShell(python *)', 'PowerShell(git *)', 'PowerShell(Get-ChildItem *)']
|
||||
'Bash(ls *)', 'Bash(dotnet *)', 'Bash(node *)', 'PowerShell(python *)', 'PowerShell(git *)', 'PowerShell(Get-ChildItem *)',
|
||||
'PowerShell(dotnet *)', 'PowerShell(node *)']
|
||||
|
||||
|
||||
def clean_env(**values):
|
||||
@@ -308,7 +309,12 @@ def check(work, task):
|
||||
hidden = work / 'bench_hidden'
|
||||
shutil.rmtree(hidden, ignore_errors=True)
|
||||
shutil.copytree(task / 'hidden', hidden)
|
||||
result = subprocess.run(CHECK, cwd=work, capture_output=True, text=True, timeout=300)
|
||||
spec = json.loads((task / 'task.json').read_text(encoding='utf-8'))
|
||||
command = CHECK
|
||||
if spec.get('check'):
|
||||
program, *arguments = spec['check']
|
||||
command = [sys.executable if program == 'python' else shutil.which(program) or program, *arguments]
|
||||
result = subprocess.run(command, cwd=work, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=600)
|
||||
shutil.rmtree(hidden, ignore_errors=True)
|
||||
return result.returncode == 0, (result.stdout + result.stderr)[-3000:]
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\..\src\Shop\Shop.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
var app = Shop.AppHost.Build(new[] { "--urls", "http://127.0.0.1:0" });
|
||||
await app.StartAsync();
|
||||
var address = app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First();
|
||||
using var client = new HttpClient { BaseAddress = new Uri(address) };
|
||||
var failures = new List<string>();
|
||||
async Task Expect(string json, int status)
|
||||
{
|
||||
var response = await client.PostAsync("/orders", new StringContent(json, Encoding.UTF8, "application/json"));
|
||||
if ((int)response.StatusCode != status) failures.Add($"{json} -> {(int)response.StatusCode}, expected {status}");
|
||||
else if (status == 400 && response.Content.Headers.ContentType?.MediaType != "application/problem+json") failures.Add($"{json} -> 400 without problem details");
|
||||
}
|
||||
await Expect("{\"sku\":\"A1\",\"quantity\":2}", 201);
|
||||
await Expect("{\"sku\":\"A1\",\"quantity\":0}", 400);
|
||||
await Expect("{\"sku\":\"A1\",\"quantity\":-3}", 400);
|
||||
await Expect("{\"sku\":\"\",\"quantity\":1}", 400);
|
||||
await app.StopAsync();
|
||||
foreach (var failure in failures) Console.Error.WriteLine("FAIL " + failure);
|
||||
return failures.Count == 0 ? 0 : 1;
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Shop;
|
||||
|
||||
public static class AppHost
|
||||
{
|
||||
// Integration tests start the application through Build.
|
||||
public static WebApplication Build(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
var orders = new List<Order>();
|
||||
app.MapPost("/orders", (Order order) =>
|
||||
{
|
||||
orders.Add(order);
|
||||
return Results.Created($"/orders/{orders.Count}", order);
|
||||
});
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
public record Order(string Sku, int Quantity);
|
||||
@@ -0,0 +1 @@
|
||||
Shop.AppHost.Build(args).Run();
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "POST /orders accepts orders with a zero or negative quantity and orders with an empty SKU. Reject those with HTTP 400 and a validation problem response; valid orders must still return 201.",
|
||||
"check": [
|
||||
"dotnet",
|
||||
"run",
|
||||
"--project",
|
||||
"bench_hidden/Check",
|
||||
"--nologo"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "C#/ASP.NET change"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Reports\Reports.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using Reports;
|
||||
|
||||
var failures = new List<string>();
|
||||
var builder = new ReportBuilder();
|
||||
var complete = await builder.BuildAsync(new[] { "a", "b" });
|
||||
if (complete != "## a\n## b") failures.Add($"unexpected output {complete}");
|
||||
using var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(150));
|
||||
var watch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await builder.BuildAsync(Enumerable.Range(0, 50).Select(number => number.ToString()), source.Token);
|
||||
failures.Add("no cancellation");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (watch.ElapsedMilliseconds > 1000) failures.Add($"cancelled only after {watch.ElapsedMilliseconds} ms");
|
||||
}
|
||||
foreach (var failure in failures) Console.Error.WriteLine("FAIL " + failure);
|
||||
return failures.Count == 0 ? 0 : 1;
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Reports;
|
||||
|
||||
public sealed class ReportBuilder
|
||||
{
|
||||
public async Task<string> BuildAsync(IEnumerable<string> sections, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var section in sections)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
parts.Add($"## {section}");
|
||||
}
|
||||
return string.Join("\n", parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "Cancelling a report build does nothing: BuildAsync keeps running until every section is rendered. Make it honor cancellation promptly by throwing OperationCanceledException.",
|
||||
"check": [
|
||||
"dotnet",
|
||||
"run",
|
||||
"--project",
|
||||
"bench_hidden/Check",
|
||||
"--nologo"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "C#/ASP.NET change"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<ProjectReference Include="..\..\src\Crm\Crm.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
var app = Crm.AppHost.Build(new[] { "--urls", "http://127.0.0.1:0" });
|
||||
await app.StartAsync();
|
||||
var address = app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()!.Addresses.First();
|
||||
using var client = new HttpClient { BaseAddress = new Uri(address) };
|
||||
var failures = new List<string>();
|
||||
var known = await client.GetAsync("/customers/1");
|
||||
if (known.StatusCode != HttpStatusCode.OK || !(await known.Content.ReadAsStringAsync()).Contains("Ada")) failures.Add($"known customer -> {(int)known.StatusCode}");
|
||||
var unknown = await client.GetAsync("/customers/42");
|
||||
if (unknown.StatusCode != HttpStatusCode.NotFound) failures.Add($"unknown customer -> {(int)unknown.StatusCode}");
|
||||
await app.StopAsync();
|
||||
foreach (var failure in failures) Console.Error.WriteLine("FAIL " + failure);
|
||||
return failures.Count == 0 ? 0 : 1;
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Crm;
|
||||
|
||||
public static class AppHost
|
||||
{
|
||||
// Integration tests start the application through Build.
|
||||
public static WebApplication Build(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
var customers = new Dictionary<int, Customer> { [1] = new(1, "Ada") };
|
||||
app.MapGet("/customers/{id:int}", (int id) => customers[id]);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
public record Customer(int Id, string Name);
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
Crm.AppHost.Build(args).Run();
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "Requesting an unknown customer with GET /customers/{id} returns HTTP 500. Return 404 instead, and keep known customers working.",
|
||||
"check": [
|
||||
"dotnet",
|
||||
"run",
|
||||
"--project",
|
||||
"bench_hidden/Check",
|
||||
"--nologo"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "C#/ASP.NET change"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { overlaps } from '../src/booking.ts';
|
||||
|
||||
test('back-to-back bookings do not overlap', () => {
|
||||
assert.equal(overlaps({ start: 9, end: 10 }, { start: 10, end: 11 }), false);
|
||||
assert.equal(overlaps({ start: 10, end: 11 }, { start: 9, end: 10 }), false);
|
||||
});
|
||||
|
||||
test('real overlaps are detected', () => {
|
||||
assert.equal(overlaps({ start: 9, end: 11 }, { start: 10, end: 12 }), true);
|
||||
assert.equal(overlaps({ start: 9, end: 12 }, { start: 10, end: 11 }), true);
|
||||
assert.equal(overlaps({ start: 9, end: 10 }, { start: 11, end: 12 }), false);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "booking",
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface Booking {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export function overlaps(a: Booking, b: Booking): boolean {
|
||||
return a.start <= b.end && b.start <= a.end;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "Back-to-back bookings, where one ends exactly when the next starts, are wrongly reported as overlapping. Fix overlaps() without breaking real overlaps.",
|
||||
"check": [
|
||||
"node",
|
||||
"--experimental-strip-types",
|
||||
"--no-warnings",
|
||||
"--test",
|
||||
"bench_hidden/check.test.ts"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "TypeScript change"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mergeConfig } from '../src/config.ts';
|
||||
|
||||
test('nested objects merge deeply', () => {
|
||||
const base = { server: { host: 'a', port: 1, tls: { on: true } }, tags: ['x'] };
|
||||
const override = { server: { port: 2, tls: { cert: 'c' } }, tags: ['y', 'z'] };
|
||||
assert.deepEqual(mergeConfig(base, override), { server: { host: 'a', port: 2, tls: { on: true, cert: 'c' } }, tags: ['y', 'z'] });
|
||||
});
|
||||
|
||||
test('inputs are not mutated', () => {
|
||||
const base = { a: { b: 1 } };
|
||||
const override = { a: { c: 2 } };
|
||||
mergeConfig(base, override);
|
||||
assert.deepEqual(base, { a: { b: 1 } });
|
||||
assert.deepEqual(override, { a: { c: 2 } });
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "config",
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Config = { [key: string]: unknown };
|
||||
|
||||
export function mergeConfig(base: Config, override: Config): Config {
|
||||
return { ...base, ...override };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "mergeConfig() drops nested settings from the base configuration when the override sets only some keys of a nested object. Merge nested objects deeply; arrays from the override replace base arrays, and neither input may be mutated.",
|
||||
"check": [
|
||||
"node",
|
||||
"--experimental-strip-types",
|
||||
"--no-warnings",
|
||||
"--test",
|
||||
"bench_hidden/check.test.ts"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "TypeScript change"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { test, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { debounce } from '../src/debounce.ts';
|
||||
|
||||
test('trailing call uses the latest arguments', () => {
|
||||
mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const calls: number[][] = [];
|
||||
const run = debounce((value: number) => calls.push([value]), 50);
|
||||
run(1); run(2); run(3);
|
||||
mock.timers.tick(49);
|
||||
assert.deepEqual(calls, []);
|
||||
mock.timers.tick(1);
|
||||
assert.deepEqual(calls, [[3]]);
|
||||
run(4);
|
||||
mock.timers.tick(50);
|
||||
assert.deepEqual(calls, [[3], [4]]);
|
||||
mock.timers.reset();
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "timing",
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function debounce<T extends unknown[]>(fn: (...args: T) => void, wait: number): (...args: T) => void {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let pending: T | undefined;
|
||||
return (...args: T) => {
|
||||
if (pending === undefined) {
|
||||
pending = args;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const call = pending as T;
|
||||
pending = undefined;
|
||||
fn(...call);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"prompt": "debounce() calls the wrapped function with the arguments of the first call in a burst instead of the last one. Fix it so the trailing call uses the latest arguments.",
|
||||
"check": [
|
||||
"node",
|
||||
"--experimental-strip-types",
|
||||
"--no-warnings",
|
||||
"--test",
|
||||
"bench_hidden/check.test.ts"
|
||||
],
|
||||
"workflow": {
|
||||
"kind": "TypeScript change"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user