-
Notifications
You must be signed in to change notification settings - Fork 74
feat(dank): add cancellable / max-results variants of GenerateAtFixedLength (#285) #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6d7f08f
de3cc93
f47dc5a
da9b8f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package dank | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| // regex that matches strings of the form "a[0-2]" (3 strings: a0, a1, a2). | ||
| const smallRegex = "a[0-2]" | ||
|
|
||
| // regex that explodes: 5 alphas anywhere in a 5-char window. The fixed-length | ||
| // generation walk is bounded by the alphabet size (~39) raised to fixedLen, so | ||
| // fixedLen=6 here yields tens of millions of strings — enough that any of | ||
| // the bounded variants should bail before the unbounded one would. | ||
| const explodingRegex = "[a-z][a-z][a-z][a-z][a-z][0-9]" | ||
|
|
||
| func TestGenerateAtFixedLength_BackwardsCompat(t *testing.T) { | ||
| d := NewDankEncoder(smallRegex, 16) | ||
| got := d.GenerateAtFixedLength(2) | ||
| want := []string{"a0", "a1", "a2"} | ||
| if !equalStringSlices(got, want) { | ||
| t.Fatalf("GenerateAtFixedLength(2) = %v, want %v", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestGenerateAtFixedLengthWithLimit_HitsCap(t *testing.T) { | ||
| d := NewDankEncoder(smallRegex, 16) | ||
| got, err := d.GenerateAtFixedLengthWithLimit(2, 2) | ||
| if !errors.Is(err, ErrResultLimitReached) { | ||
| t.Fatalf("expected ErrResultLimitReached, got %v", err) | ||
| } | ||
| if len(got) != 2 { | ||
| t.Fatalf("expected exactly 2 results at the cap, got %d (%v)", len(got), got) | ||
| } | ||
| } | ||
|
|
||
| func TestGenerateAtFixedLengthWithLimit_NoCap(t *testing.T) { | ||
| d := NewDankEncoder(smallRegex, 16) | ||
| got, err := d.GenerateAtFixedLengthWithLimit(2, 0) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error with maxResults=0: %v", err) | ||
| } | ||
| if len(got) != 3 { | ||
| t.Fatalf("expected 3 results without cap, got %d (%v)", len(got), got) | ||
| } | ||
| } | ||
|
Comment on lines
+28
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert the actual result slices, not just the lengths. These checks would still pass if the generator returned the wrong strings in sorted order. Verifying the exact capped and uncapped slices would better lock down the new limit behavior. ♻️ Suggested tightening for the tests func TestGenerateAtFixedLengthWithLimit_HitsCap(t *testing.T) {
d := NewDankEncoder(smallRegex, 16)
got, err := d.GenerateAtFixedLengthWithLimit(2, 2)
if !errors.Is(err, ErrResultLimitReached) {
t.Fatalf("expected ErrResultLimitReached, got %v", err)
}
- if len(got) != 2 {
- t.Fatalf("expected exactly 2 results at the cap, got %d (%v)", len(got), got)
+ want := []string{"a0", "a1"}
+ if !equalStringSlices(got, want) {
+ t.Fatalf("GenerateAtFixedLengthWithLimit(2, 2) = %v, want %v", got, want)
}
}
func TestGenerateAtFixedLengthWithLimit_NoCap(t *testing.T) {
d := NewDankEncoder(smallRegex, 16)
got, err := d.GenerateAtFixedLengthWithLimit(2, 0)
if err != nil {
t.Fatalf("unexpected error with maxResults=0: %v", err)
}
- if len(got) != 3 {
- t.Fatalf("expected 3 results without cap, got %d (%v)", len(got), got)
+ want := []string{"a0", "a1", "a2"}
+ if !equalStringSlices(got, want) {
+ t.Fatalf("GenerateAtFixedLengthWithLimit(2, 0) = %v, want %v", got, want)
}
}🤖 Prompt for AI Agents |
||
|
|
||
| func TestGenerateAtFixedLengthWithLimit_NegativeFixedLen(t *testing.T) { | ||
| d := NewDankEncoder(smallRegex, 16) | ||
| got, err := d.GenerateAtFixedLengthWithLimit(-1, 10) | ||
| if !errors.Is(err, ErrInvalidFixedLength) { | ||
| t.Fatalf("expected ErrInvalidFixedLength for negative fixedLen, got %v", err) | ||
| } | ||
| if len(got) != 0 { | ||
| t.Fatalf("expected empty slice on validation failure, got %v", got) | ||
| } | ||
| } | ||
|
|
||
| func TestGenerateAtFixedLengthWithContext_NilContext(t *testing.T) { | ||
| d := NewDankEncoder(smallRegex, 16) | ||
| // Passing a nil context must not panic; the public entry point normalises | ||
| // it to context.Background() before reaching the recursive ctx.Err() call. | ||
| // Assign through a typed var so staticcheck SA1012 doesn't flag the | ||
| // literal nil at the call site - we are explicitly exercising the guard. | ||
| var nilCtx context.Context | ||
| got, err := d.GenerateAtFixedLengthWithContext(nilCtx, 2, 0) | ||
| if err != nil { | ||
| t.Fatalf("expected nil error with nil ctx, got %v", err) | ||
| } | ||
| want := []string{"a0", "a1", "a2"} | ||
| if !equalStringSlices(got, want) { | ||
| t.Fatalf("GenerateAtFixedLengthWithContext(nil, 2, 0) = %v, want %v", got, want) | ||
| } | ||
| } | ||
|
|
||
| func TestGenerateAtFixedLengthWithContext_Cancellation(t *testing.T) { | ||
| d := NewDankEncoder(explodingRegex, 16) | ||
| ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond) | ||
| defer cancel() | ||
|
|
||
| start := time.Now() | ||
| got, err := d.GenerateAtFixedLengthWithContext(ctx, 6, 0) | ||
| elapsed := time.Since(start) | ||
|
|
||
| if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) { | ||
| t.Fatalf("expected context cancellation error, got %v", err) | ||
| } | ||
| if elapsed > 500*time.Millisecond { | ||
| t.Fatalf("DFS did not honour context deadline (took %s)", elapsed) | ||
| } | ||
| // Partial result slice should still be returned and sorted. | ||
| if !isSorted(got) { | ||
| t.Fatalf("partial results should be sorted, got %v", got[:min(10, len(got))]) | ||
| } | ||
| } | ||
|
Comment on lines
+78
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prove the cancellation path preserves partial output. An empty slice is still sorted, so this test can pass even if the implementation drops all work on cancellation. Add an assertion that some partial results were produced, or otherwise check a deterministic prefix, so the test actually exercises the contract. ♻️ Suggested tightening for the cancellation test func TestGenerateAtFixedLengthWithContext_Cancellation(t *testing.T) {
d := NewDankEncoder(explodingRegex, 16)
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
defer cancel()
@@
if elapsed > 500*time.Millisecond {
t.Fatalf("DFS did not honour context deadline (took %s)", elapsed)
}
// Partial result slice should still be returned and sorted.
+ if len(got) == 0 {
+ t.Fatal("expected partial results before cancellation")
+ }
if !isSorted(got) {
t.Fatalf("partial results should be sorted, got %v", got[:min(10, len(got))])
}
}🤖 Prompt for AI Agents |
||
|
|
||
| func equalStringSlices(a, b []string) bool { | ||
| if len(a) != len(b) { | ||
| return false | ||
| } | ||
| for i := range a { | ||
| if a[i] != b[i] { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func isSorted(s []string) bool { | ||
| for i := 1; i < len(s); i++ { | ||
| if s[i-1] > s[i] { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func min(a, b int) int { | ||
| if a < b { | ||
| return a | ||
| } | ||
| return b | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.