Skip to content

Commit 7039486

Browse files
authored
Merge pull request #236 from Zac-HD/patching
Fix and improve patch computation
2 parents 208d12e + ed21dcc commit 7039486

File tree

5 files changed

+77
-55
lines changed

5 files changed

+77
-55
lines changed

src/hypofuzz/dashboard/patching.py

Lines changed: 66 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,49 @@
11
import threading
22
from collections import defaultdict
3-
from functools import lru_cache
3+
from collections.abc import Sequence
44
from queue import Empty, Queue
5-
from typing import Any, Literal, Optional
5+
from typing import TYPE_CHECKING, Any, Literal, Optional
66

7-
from hypothesis.extra._patching import (
8-
get_patch_for as _get_patch_for,
9-
make_patch as _make_patch,
10-
)
7+
from hypothesis.extra._patching import get_patch_for, make_patch as _make_patch
8+
from sortedcontainers import SortedList
119

1210
from hypofuzz import __version__
1311
from hypofuzz.database import Observation
1412

15-
COVERING_VIA = "covering example"
16-
FAILING_VIA = "discovered failure"
13+
if TYPE_CHECKING:
14+
from typing import TypeAlias
15+
16+
# we have a two tiered structure.
17+
# * First, we store the list of test case reprs corresponding to the list of
18+
# @examples.
19+
# * Each time we add a new such input, we compute the new patch for the entire
20+
# list.
21+
1722
# nodeid: {
18-
# "covering": [(fname, before, after), ...],
19-
# "failing": [(fname, before, after), ...],
23+
# "covering": list[observation.representation],
24+
# "failing": list[observation.representation],
2025
# }
21-
# TODO this duplicates the test function contents in `before` and `after`,
22-
# we probably want a more memory-efficient representation eventually
23-
# (and a smaller win: map fname to a list of (before, after), instead of storing
24-
# each fname)
25-
PATCHES: dict[str, dict[str, list[tuple[str, str, str]]]] = defaultdict(
26-
lambda: {"covering": [], "failing": []}
26+
#
27+
# We sort by string length, as a heuristic for putting simpler examples first in
28+
# the patch.
29+
EXAMPLES: dict[str, dict[str, SortedList[str]]] = defaultdict(
30+
lambda: {"covering": SortedList(key=len), "failing": SortedList(key=len)}
2731
)
28-
get_patch_for = lru_cache(maxsize=8192)(_get_patch_for)
29-
30-
_queue: Queue = Queue()
32+
# nodeid: {
33+
# "covering": patch,
34+
# "failing": patch,
35+
# }
36+
PATCHES: dict[str, dict[str, Optional[str]]] = defaultdict(
37+
lambda: {"covering": None, "failing": None}
38+
)
39+
VIA = {"covering": "covering example", "failing": "discovered failure"}
40+
COMMIT_MESSAGE = {
41+
"covering": "add covering examples",
42+
"failing": "add failing examples",
43+
}
44+
45+
ObservationTypeT: "TypeAlias" = Literal["covering", "failing"]
46+
_queue: Queue[tuple[Any, str, Observation, ObservationTypeT]] = Queue()
3147
_thread: Optional[threading.Thread] = None
3248

3349

@@ -36,51 +52,45 @@ def add_patch(
3652
test_function: Any,
3753
nodeid: str,
3854
observation: Observation,
39-
observation_type: Literal["covering", "failing"],
55+
observation_type: ObservationTypeT,
4056
) -> None:
4157
_queue.put((test_function, nodeid, observation, observation_type))
4258

4359

44-
@lru_cache(maxsize=1024)
45-
def make_patch(triples: tuple[tuple[str, str, str]], *, msg: str) -> str:
60+
def make_patch(
61+
function: Any, examples: Sequence[str], observation_type: ObservationTypeT
62+
) -> Optional[str]:
63+
via = VIA[observation_type]
64+
triple = get_patch_for(function, examples=[(example, via) for example in examples])
65+
if triple is None:
66+
return None
67+
68+
commit_message = COMMIT_MESSAGE[observation_type]
4669
return _make_patch(
47-
triples,
48-
msg=msg,
70+
(triple,),
71+
msg=commit_message,
4972
author=f"HypoFuzz {__version__} <no-reply@hypofuzz.com>",
5073
)
5174

5275

53-
def failing_patch(nodeid: str) -> Optional[str]:
54-
failing = PATCHES[nodeid]["failing"]
55-
return make_patch(tuple(failing), msg="add failing examples") if failing else None
56-
57-
58-
def covering_patch(nodeid: str) -> Optional[str]:
59-
covering = PATCHES[nodeid]["covering"]
60-
return (
61-
make_patch(tuple(covering), msg="add covering examples") if covering else None
62-
)
63-
64-
6576
def _worker() -> None:
77+
# TODO We might optimize this by checking each function ahead of time for known
78+
# reasons why a patch would fail, for instance using st.data in the signature,
79+
# and then early-returning here before calling get_patch_for.
6680
while True:
6781
try:
68-
item = _queue.get(timeout=1.0)
82+
test_function, nodeid, observation, observation_type = _queue.get(
83+
timeout=1.0
84+
)
6985
except Empty:
7086
continue
7187

72-
test_function, nodeid, observation, observation_type = item
73-
74-
via = COVERING_VIA if observation_type == "covering" else FAILING_VIA
75-
# If this thread ends up using significant resources, we might optimize
76-
# this by checking each function ahead of time for known reasons why a
77-
# patch would fail, for instance using st.data in the signature, and then
78-
# simply discarding those here entirely.
79-
patch = get_patch_for(
80-
test_function, ((observation.representation, via),), strip_via=via
88+
examples = EXAMPLES[nodeid][observation_type]
89+
examples.add(observation.representation)
90+
PATCHES[nodeid][observation_type] = make_patch(
91+
test_function, examples, observation_type
8192
)
82-
if patch is not None:
83-
PATCHES[nodeid][observation_type].append(patch)
93+
8494
_queue.task_done()
8595

8696

@@ -90,3 +100,11 @@ def start_patching_thread() -> None:
90100

91101
_thread = threading.Thread(target=_worker, daemon=True)
92102
_thread.start()
103+
104+
105+
def failing_patch(nodeid: str) -> Optional[str]:
106+
return PATCHES[nodeid]["failing"]
107+
108+
109+
def covering_patch(nodeid: str) -> Optional[str]:
110+
return PATCHES[nodeid]["covering"]

src/hypofuzz/frontend/src/tyche/Representation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function Representation({
6262

6363
return (
6464
<TycheSection
65-
title="String representations"
65+
title="Test cases"
6666
defaultState="closed"
6767
onStateChange={state => {
6868
if (state === "open") {

src/hypofuzz/frontend/src/tyche/Samples.tsx renamed to src/hypofuzz/frontend/src/tyche/Summary.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TYCHE_COLOR } from "src/tyche/Tyche"
33
import { TycheSection } from "src/tyche/TycheSection"
44
import { Observation } from "src/types/dashboard"
55

6-
export function Samples({
6+
export function Summary({
77
observations,
88
}: {
99
observations: { raw: Observation[]; filtered: Observation[] }
@@ -27,9 +27,9 @@ export function Samples({
2727
}
2828

2929
return (
30-
<TycheSection title="Samples">
30+
<TycheSection title="Summary">
3131
<MosaicChart
32-
name="samples"
32+
name="summary"
3333
observations={observations}
3434
verticalAxis={[
3535
["Passed", obs => obs.status === "passed"],

src/hypofuzz/frontend/src/tyche/Tyche.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Features } from "src/tyche/Features"
33
import { FilterProvider, useFilters } from "src/tyche/FilterContext"
44
import { Filters } from "src/tyche/Filters"
55
import { Representation } from "src/tyche/Representation"
6-
import { Samples } from "src/tyche/Samples"
6+
import { Summary } from "src/tyche/Summary"
77
import { Test } from "src/types/test"
88

99
export const PRESENT_STRING = "Present"
@@ -78,7 +78,7 @@ function TycheInner({ test }: { test: Test }) {
7878
</div>
7979
{observations.raw.length > 0 ? (
8080
<>
81-
<Samples observations={observations} />
81+
<Summary observations={observations} />
8282
<Features
8383
observations={observations}
8484
observationCategory={observationCategory}

src/hypofuzz/frontend/src/utils/tooltip.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ function TooltipPortal({ state }: { state: TooltipState }) {
6666

6767
const bottomEdge = top + tooltipHeight
6868
if (bottomEdge > window.innerHeight - SCREEN_MARGIN) {
69-
top = state.y + TOOLTIP_OFFSET
69+
top = state.y - tooltipHeight - TOOLTIP_OFFSET
70+
71+
if (top < SCREEN_MARGIN) {
72+
top = SCREEN_MARGIN
73+
}
7074
}
7175

7276
setPosition({ left, top })

0 commit comments

Comments
 (0)