quack-test 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quack_test-0.1.0/LICENSE +21 -0
- quack_test-0.1.0/PKG-INFO +44 -0
- quack_test-0.1.0/README.md +32 -0
- quack_test-0.1.0/pyproject.toml +18 -0
- quack_test-0.1.0/quack_test/__init__.py +15 -0
- quack_test-0.1.0/quack_test/decorators.py +147 -0
- quack_test-0.1.0/quack_test/judge.py +115 -0
- quack_test-0.1.0/quack_test.egg-info/PKG-INFO +44 -0
- quack_test-0.1.0/quack_test.egg-info/SOURCES.txt +12 -0
- quack_test-0.1.0/quack_test.egg-info/dependency_links.txt +1 -0
- quack_test-0.1.0/quack_test.egg-info/requires.txt +3 -0
- quack_test-0.1.0/quack_test.egg-info/top_level.txt +1 -0
- quack_test-0.1.0/setup.cfg +4 -0
- quack_test-0.1.0/test/test_example.py +18 -0
quack_test-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael F.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quack-test
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pytest>=7.0.0
|
|
9
|
+
Requires-Dist: openai>=1.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Quack Test
|
|
14
|
+
|
|
15
|
+
A plugin for pytest to evaluate non-deterministic agent components.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Simply pip install it
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install quack-test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
You can simply specify `@nondeterministic` for components, which are not deterministic and should be run multiple times.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import random
|
|
31
|
+
from quack_test import nondeterministic_fixture, nondeterministic_test, judge
|
|
32
|
+
|
|
33
|
+
@nondeterministic_fixture(n=5)
|
|
34
|
+
def sample_text():
|
|
35
|
+
return f"I have {random.randint(10)} apples."
|
|
36
|
+
|
|
37
|
+
@nondeterministic_test(score=0.8)
|
|
38
|
+
def test_apples(sample_text):
|
|
39
|
+
return judge(sample_text, criterion="Has more than 1 apple")
|
|
40
|
+
|
|
41
|
+
@nondeterministic_test(score=0.8)
|
|
42
|
+
def test_coal(sample_text):
|
|
43
|
+
return judge(sample_text, criterion="Has only coal")
|
|
44
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Quack Test
|
|
2
|
+
|
|
3
|
+
A plugin for pytest to evaluate non-deterministic agent components.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Simply pip install it
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install quack-test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
You can simply specify `@nondeterministic` for components, which are not deterministic and should be run multiple times.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import random
|
|
19
|
+
from quack_test import nondeterministic_fixture, nondeterministic_test, judge
|
|
20
|
+
|
|
21
|
+
@nondeterministic_fixture(n=5)
|
|
22
|
+
def sample_text():
|
|
23
|
+
return f"I have {random.randint(10)} apples."
|
|
24
|
+
|
|
25
|
+
@nondeterministic_test(score=0.8)
|
|
26
|
+
def test_apples(sample_text):
|
|
27
|
+
return judge(sample_text, criterion="Has more than 1 apple")
|
|
28
|
+
|
|
29
|
+
@nondeterministic_test(score=0.8)
|
|
30
|
+
def test_coal(sample_text):
|
|
31
|
+
return judge(sample_text, criterion="Has only coal")
|
|
32
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quack-test"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pytest>=7.0.0",
|
|
9
|
+
"openai>=1.0.0",
|
|
10
|
+
"python-dotenv>=1.0.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"ty",
|
|
16
|
+
"ruff",
|
|
17
|
+
"pre-commit",
|
|
18
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quack Test - Decorators for handling nondeterministic tests and fixtures.
|
|
3
|
+
|
|
4
|
+
Provides decorators for running fixtures and tests multiple times to handle
|
|
5
|
+
flaky behavior, particularly useful for LLM-based tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from quack_test.decorators import nondeterministic_fixture, nondeterministic_test
|
|
9
|
+
from quack_test.judge import judge
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"nondeterministic_fixture",
|
|
13
|
+
"nondeterministic_test",
|
|
14
|
+
"judge",
|
|
15
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for handling nondeterministic tests and fixtures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import pytest
|
|
7
|
+
from typing import Callable, Any, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def nondeterministic_fixture(n: int = 5):
|
|
11
|
+
"""
|
|
12
|
+
Decorator for fixtures that should be executed multiple times.
|
|
13
|
+
|
|
14
|
+
The fixture function will be executed `n` times, and all results
|
|
15
|
+
will be collected into a list. This list is then passed to tests
|
|
16
|
+
that depend on this fixture.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
n: Number of times to execute the fixture (default: 5)
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@nondeterministic_fixture(n=10)
|
|
23
|
+
def random_data():
|
|
24
|
+
return random.randint(1, 100)
|
|
25
|
+
|
|
26
|
+
# The test will receive a list of 10 random integers
|
|
27
|
+
def test_values(random_data):
|
|
28
|
+
assert len(random_data) == 10
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def decorator(func: Callable) -> Callable:
|
|
32
|
+
@functools.wraps(func)
|
|
33
|
+
def wrapper(*args, **kwargs) -> List[Any]:
|
|
34
|
+
results = []
|
|
35
|
+
for _ in range(n):
|
|
36
|
+
result = func(*args, **kwargs)
|
|
37
|
+
results.append(result)
|
|
38
|
+
return results
|
|
39
|
+
|
|
40
|
+
# Mark this as a pytest fixture
|
|
41
|
+
return pytest.fixture(wrapper)
|
|
42
|
+
|
|
43
|
+
return decorator
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def nondeterministic_test(score: float = 0.8, n: int = -1, should_fail: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
Decorator for tests that should be executed multiple times with a success threshold.
|
|
49
|
+
|
|
50
|
+
The test function will be executed multiple times (determined by the length of
|
|
51
|
+
fixture data if available, or a default number). The test must return a score
|
|
52
|
+
which will be averaged. If the average score is higher than the given score, you pass.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
score: Minimum average score that must be achieved in order to pass the test.
|
|
56
|
+
n: Number of times to run the test (if -1, uses fixture length)
|
|
57
|
+
should_fail: If True, the test passes when the score is BELOW the threshold (default: False)
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
@nondeterministic_fixture(n=10)
|
|
61
|
+
def llm_output():
|
|
62
|
+
return call_llm("Generate a greeting")
|
|
63
|
+
|
|
64
|
+
@nondeterministic_test(score=0.8)
|
|
65
|
+
def test_greeting(llm_output):
|
|
66
|
+
return judge(llm_output, criterion="Contains 'hello' or 'hi'")
|
|
67
|
+
|
|
68
|
+
@nondeterministic_test(score=0.8, should_fail=True)
|
|
69
|
+
def test_no_profanity(llm_output):
|
|
70
|
+
return judge(llm_output, criterion="Contains profanity")
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def decorator(func: Callable) -> Callable:
|
|
74
|
+
@functools.wraps(func)
|
|
75
|
+
def wrapper(*args, **kwargs) -> None:
|
|
76
|
+
# n_runs can be either given by n or defined by the length of the fixture data in args or kwargs
|
|
77
|
+
# use any list found in args or kwargs as fixture data
|
|
78
|
+
fixture_data = None
|
|
79
|
+
for arg in args:
|
|
80
|
+
if isinstance(arg, list):
|
|
81
|
+
fixture_data = arg
|
|
82
|
+
break
|
|
83
|
+
for kwarg in kwargs.values():
|
|
84
|
+
if isinstance(kwarg, list):
|
|
85
|
+
fixture_data = kwarg
|
|
86
|
+
break
|
|
87
|
+
if n > 0:
|
|
88
|
+
n_runs = n
|
|
89
|
+
elif fixture_data is not None:
|
|
90
|
+
n_runs = len(fixture_data)
|
|
91
|
+
else:
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
"Cannot determine number of runs for nondeterministic_test. "
|
|
94
|
+
"Provide a fixture that returns a list or set n explicitly."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Run the test n times
|
|
98
|
+
successes = 0
|
|
99
|
+
failures = 0
|
|
100
|
+
scores = []
|
|
101
|
+
|
|
102
|
+
for i in range(n_runs):
|
|
103
|
+
try:
|
|
104
|
+
# If we have fixture data, pass the i-th element
|
|
105
|
+
test_args = [
|
|
106
|
+
arg[i] if isinstance(arg, list) and len(arg) == n_runs else arg
|
|
107
|
+
for arg in args
|
|
108
|
+
]
|
|
109
|
+
test_kwargs = {
|
|
110
|
+
k: (v[i] if isinstance(v, list) and len(v) == n_runs else v)
|
|
111
|
+
for k, v in kwargs.items()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
result = float(func(*test_args, **test_kwargs))
|
|
115
|
+
scores.append(result)
|
|
116
|
+
|
|
117
|
+
if result > score:
|
|
118
|
+
successes += 1
|
|
119
|
+
else:
|
|
120
|
+
failures += 1
|
|
121
|
+
except Exception:
|
|
122
|
+
failures += 1
|
|
123
|
+
scores.append(0)
|
|
124
|
+
|
|
125
|
+
# Calculate success rate
|
|
126
|
+
success_rate = successes / n_runs
|
|
127
|
+
achieved_score = sum(scores) / n_runs
|
|
128
|
+
|
|
129
|
+
# Assert the success rate meets the threshold
|
|
130
|
+
if should_fail:
|
|
131
|
+
# For should_fail tests, we expect the score to be BELOW the threshold
|
|
132
|
+
assert achieved_score < score, (
|
|
133
|
+
f"Test expected to fail but succeeded. "
|
|
134
|
+
f"Score: {achieved_score:.2} (required: < {score:.2}), "
|
|
135
|
+
f"Success rate: {success_rate:.2%} ({successes}/{n_runs})"
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
# Normal tests expect the score to be AT OR ABOVE the threshold
|
|
139
|
+
assert achieved_score >= score, (
|
|
140
|
+
f"Test failed to meet success threshold. "
|
|
141
|
+
f"Score: {achieved_score:.2} (required: {score:.2}), "
|
|
142
|
+
f"Success rate: {success_rate:.2%} ({successes}/{n_runs})"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return wrapper
|
|
146
|
+
|
|
147
|
+
return decorator
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Judge function for evaluating test outputs against criteria.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from functools import cache
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from openai import OpenAI, AzureOpenAI
|
|
10
|
+
|
|
11
|
+
# Load environment variables
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
# Get the directory where this file is located
|
|
15
|
+
_PROMPTS_DIR = Path(__file__).parent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cache
|
|
19
|
+
def _load_prompt_template(filepath: Path) -> str:
|
|
20
|
+
"""Load a prompt template from a markdown file with caching."""
|
|
21
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
22
|
+
return f.read()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cache
|
|
26
|
+
def _get_client():
|
|
27
|
+
"""Create and cache the OpenAI or Azure OpenAI client."""
|
|
28
|
+
provider = os.getenv("OPENAI_PROVIDER", "OpenAI")
|
|
29
|
+
|
|
30
|
+
if provider == "AzureOpenAI":
|
|
31
|
+
return AzureOpenAI(
|
|
32
|
+
api_key=os.environ["OPENAI_API_KEY"],
|
|
33
|
+
base_url=os.environ["OPENAI_ENDPOINT"],
|
|
34
|
+
api_version=os.getenv("OPENAI_API_VERSION", "2024-10-21"),
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
return OpenAI(
|
|
38
|
+
api_key=os.environ["OPENAI_API_KEY"],
|
|
39
|
+
base_url=os.environ["OPENAI_ENDPOINT"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def judge(
|
|
44
|
+
text: str,
|
|
45
|
+
criterion: str = "",
|
|
46
|
+
gt: str = "",
|
|
47
|
+
prompt_template: Optional[str] = None
|
|
48
|
+
) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Evaluate whether an text meets a given criterion or matches the ground truth.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
text: The text to evaluate (typically a string)
|
|
54
|
+
criterion: (Optional) The criterion to check against
|
|
55
|
+
gt: (Optional) A ground truth which the text should match
|
|
56
|
+
prompt_template: (Optional) Custom prompt template string with {text}, {criterion}, and/or {gt} placeholders.
|
|
57
|
+
If not provided, loads from criterion_prompt.md or gt_prompt.md
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
float: A score how well it matches the gt or criterion
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
>>> judge("I have 5 apples", criterion="Has more than 1 apple")
|
|
64
|
+
5.0
|
|
65
|
+
|
|
66
|
+
>>> judge("I have coal", gt="I have some coal")
|
|
67
|
+
5.0
|
|
68
|
+
|
|
69
|
+
>>> judge("No apples here", criterion="Has more than 1 apple")
|
|
70
|
+
0.0
|
|
71
|
+
|
|
72
|
+
>>> judge("I have 5 apples", criterion="Has apples",
|
|
73
|
+
... prompt_template="Does '{text}' meet '{criterion}'? Score 0-1:")
|
|
74
|
+
5.0
|
|
75
|
+
"""
|
|
76
|
+
# Build the prompt based on what's provided
|
|
77
|
+
if prompt_template:
|
|
78
|
+
# Use custom prompt template
|
|
79
|
+
prompt = prompt_template.format(criterion=criterion, gt=gt)
|
|
80
|
+
elif criterion and gt:
|
|
81
|
+
raise ValueError("Criterion and gt cannot be set at the same time without a custom template.")
|
|
82
|
+
elif criterion:
|
|
83
|
+
# Load criterion prompt template
|
|
84
|
+
template = _load_prompt_template(_PROMPTS_DIR / "prompt_criterion.md")
|
|
85
|
+
prompt = template.format(criterion=criterion)
|
|
86
|
+
elif gt:
|
|
87
|
+
# Load ground truth prompt template
|
|
88
|
+
template = _load_prompt_template(_PROMPTS_DIR / "prompt_gt.md")
|
|
89
|
+
prompt = template.format(gt=gt)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError("Either criterion or gt must be provided")
|
|
92
|
+
|
|
93
|
+
# Make the API call
|
|
94
|
+
client = _get_client()
|
|
95
|
+
response = client.chat.completions.create(
|
|
96
|
+
model=os.getenv("OPENAI_MODEL_NAME", "gpt-4o"),
|
|
97
|
+
messages=[
|
|
98
|
+
{"role": "system", "content": prompt},
|
|
99
|
+
{"role": "user", "content": text}
|
|
100
|
+
],
|
|
101
|
+
temperature=0.0,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Extract and return the score
|
|
105
|
+
content = response.choices[0].message.content
|
|
106
|
+
if content is None:
|
|
107
|
+
raise ValueError("Received empty response from API")
|
|
108
|
+
|
|
109
|
+
score_text = content.strip()
|
|
110
|
+
try:
|
|
111
|
+
score = float(score_text)
|
|
112
|
+
# Clamp the score between 0.0 and 1.0
|
|
113
|
+
return score
|
|
114
|
+
except ValueError:
|
|
115
|
+
raise ValueError(f"Failed to parse score from response: {score_text}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quack-test
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pytest>=7.0.0
|
|
9
|
+
Requires-Dist: openai>=1.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Quack Test
|
|
14
|
+
|
|
15
|
+
A plugin for pytest to evaluate non-deterministic agent components.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Simply pip install it
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install quack-test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
You can simply specify `@nondeterministic` for components, which are not deterministic and should be run multiple times.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import random
|
|
31
|
+
from quack_test import nondeterministic_fixture, nondeterministic_test, judge
|
|
32
|
+
|
|
33
|
+
@nondeterministic_fixture(n=5)
|
|
34
|
+
def sample_text():
|
|
35
|
+
return f"I have {random.randint(10)} apples."
|
|
36
|
+
|
|
37
|
+
@nondeterministic_test(score=0.8)
|
|
38
|
+
def test_apples(sample_text):
|
|
39
|
+
return judge(sample_text, criterion="Has more than 1 apple")
|
|
40
|
+
|
|
41
|
+
@nondeterministic_test(score=0.8)
|
|
42
|
+
def test_coal(sample_text):
|
|
43
|
+
return judge(sample_text, criterion="Has only coal")
|
|
44
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
quack_test/__init__.py
|
|
5
|
+
quack_test/decorators.py
|
|
6
|
+
quack_test/judge.py
|
|
7
|
+
quack_test.egg-info/PKG-INFO
|
|
8
|
+
quack_test.egg-info/SOURCES.txt
|
|
9
|
+
quack_test.egg-info/dependency_links.txt
|
|
10
|
+
quack_test.egg-info/requires.txt
|
|
11
|
+
quack_test.egg-info/top_level.txt
|
|
12
|
+
test/test_example.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
quack_test
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from quack_test import nondeterministic_fixture, nondeterministic_test, judge
|
|
3
|
+
|
|
4
|
+
@nondeterministic_fixture(n=5)
|
|
5
|
+
def sample_text():
|
|
6
|
+
return f"I have {random.randint(0, 10)} apples."
|
|
7
|
+
|
|
8
|
+
@nondeterministic_test(score=0.8)
|
|
9
|
+
def test_apples(sample_text):
|
|
10
|
+
return judge(sample_text, criterion="Has more than 1 apple")
|
|
11
|
+
|
|
12
|
+
@nondeterministic_test(score=0.8)
|
|
13
|
+
def test_apples_gt(sample_text):
|
|
14
|
+
return judge(sample_text, gt="I have N (0-10) apples")
|
|
15
|
+
|
|
16
|
+
@nondeterministic_test(score=0.8, should_fail=True)
|
|
17
|
+
def test_coal(sample_text):
|
|
18
|
+
return judge(sample_text, criterion="Has only coal")
|